 /*******************************************************************************
  * Copyright (c) 2002, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui.internal.cheatsheets.registry;

 import com.ibm.icu.text.Collator;
 import java.util.ArrayList ;
 import java.util.HashMap ;
 import java.util.Iterator ;
 import java.util.Map ;
 import java.util.Set ;
 import java.util.StringTokenizer ;

 import org.eclipse.core.runtime.*;
 import org.eclipse.ui.internal.cheatsheets.*;
 import org.eclipse.ui.model.AdaptableList;

 /**
  * Instances access the registry that is provided at creation time
  * in order to determine the contained CheatSheet Contents
  */
 public class CheatSheetRegistryReader extends RegistryReader implements IRegistryChangeListener {

     private class CategoryNode {
         private Category category;
         private String path;
         public CategoryNode(Category cat) {
             category = cat;
             path = ICheatSheetResource.EMPTY_STRING;
             String [] categoryPath = category.getParentPath();
             if (categoryPath != null) {
                 for (int nX = 0; nX < categoryPath.length; nX++) {
                     path += categoryPath[nX] + '/';
                 }
             }
             path += cat.getId();
         }
         public Category getCategory() {
             return category;
         }
         public String getPath() {
             return path;
         }
     }

     /**
      * Represents a taskEditor entry in the registry
      */
     public class TaskEditorNode {
         private String className;
         private String iconPath;
         private String id;
         private String pluginId;
         public void setClassName(String className) {
             this.className = className;
         }
         public String getClassName() {
             return className;
         }
         public void setIconPath(String iconPath) {
             this.iconPath = iconPath;
         }
         public String getIconPath() {
             return iconPath;
         }
         public void setId(String id) {
             this.id = id;
         }
         public String getId() {
             return id;
         }
         public void setPluginId(String pluginId) {
             this.pluginId = pluginId;
         }
         public String getPluginId() {
             return pluginId;
         }
     }
     
     /**
      * Represents a taskExplorer entry in the registry
      */
     public class TaskExplorerNode {
         private String className;
         private String iconPath;
         private String name;
         private String id;
         private String pluginId;
         public void setClassName(String className) {
             this.className = className;
         }
         public String getClassName() {
             return className;
         }
         public void setIconPath(String iconPath) {
             this.iconPath = iconPath;
         }
         public String getIconPath() {
             return iconPath;
         }
         public void setName(String name) {
             this.name = name;
         }
         public String getName() {
             return name;
         }
         public void setId(String id) {
             this.id = id;
         }
         public String getId() {
             return id;
         }
         public void setPluginId(String pluginId) {
             this.pluginId = pluginId;
         }
         public String getPluginId() {
             return pluginId;
         }
     }

     // constants
 private final static String ATT_CATEGORY = "category"; //$NON-NLS-1$
 public final static String ATT_CONTENTFILE = "contentFile"; //$NON-NLS-1$
 protected final static String ATT_ICON = "icon"; //$NON-NLS-1$
 protected final static String ATT_ID = "id"; //$NON-NLS-1$
 protected final static String ATT_LISTENERCLASS = "listener"; //$NON-NLS-1$
 protected final static String ATT_NAME = "name"; //$NON-NLS-1$
 protected final static String ATT_CLASS = "class"; //$NON-NLS-1$
 private final static String ATT_COMPOSITE = "composite"; //$NON-NLS-1$
 private final static String CATEGORY_SEPARATOR = "/"; //$NON-NLS-1$
 private final static String ATT_ITEM_ATTRIBUTE = "itemAttribute"; //$NON-NLS-1$
 private static CheatSheetRegistryReader instance;
     private final static String TAG_CATEGORY = "category"; //$NON-NLS-1$
 public final static String TAG_CHEATSHEET = "cheatsheet"; //$NON-NLS-1$
 protected final static String TAG_ITEM_EXTENSION = "itemExtension"; //$NON-NLS-1$
 protected final static String TAG_TASK_EDITOR = "taskEditor"; //$NON-NLS-1$
 protected final static String TAG_TASK_EXPLORER = "taskExplorer"; //$NON-NLS-1$
 protected final static String trueString = "TRUE"; //$NON-NLS-1$
 private final static String UNCATEGORIZED_CHEATSHEET_CATEGORY = "org.eclipse.ui.Other"; //$NON-NLS-1$
 private final static String UNCATEGORIZED_CHEATSHEET_CATEGORY_LABEL = Messages.CHEAT_SHEET_OTHER_CATEGORY;
     public final static String CHEAT_SHEET_CONTENT = "cheatSheetContent"; //$NON-NLS-1$

     /**
      * Returns a list of cheatsheets, project and not.
      *
      * The return value for this method is cached since computing its value
      * requires non-trivial work.
      */
     public static CheatSheetRegistryReader getInstance() {
         if (instance == null) {
             instance = new CheatSheetRegistryReader();
             IExtensionRegistry xregistry = Platform.getExtensionRegistry();
             xregistry.addRegistryChangeListener(instance, ICheatSheetResource.CHEAT_SHEET_PLUGIN_ID);
         }

         return instance;
     }

     protected ArrayList cheatsheetItemExtensions;
     protected AdaptableList cheatsheets;
     private ArrayList deferCategories = null;
     private ArrayList deferCheatSheets = null;
     private final String csItemExtension = "cheatSheetItemExtension"; //$NON-NLS-1$
 protected Map taskExplorers = new HashMap ();
     protected Map taskEditors = new HashMap ();

     /**
      * Create an instance of this class.
      */
     private CheatSheetRegistryReader() {
     }

     /**
      * Adds new cheatsheet to the provided collection. Override to
      * provide more logic.
      * <p>
      * This implementation uses a defering strategy. For more info see
      * <code>readCheatSheets</code>.
      * </p>
      */
     protected void addNewElementToResult(CheatSheetElement element, IConfigurationElement config, AdaptableList result) {
         deferCheatSheet(element);
     }

     /**
      * Returns a new CheatSheetElement configured according to the parameters
      * contained in the passed Registry.
      *
      * May answer null if there was not enough information in the Extension to create
      * an adequate cheatsheet
      */
     protected CheatSheetElement createCheatSheetElement(IConfigurationElement element) {
         // CheatSheetElements must have a name attribute
 String nameString = element.getAttribute(ATT_NAME);
         if (nameString == null) {
             logMissingAttribute(element, ATT_NAME);
             return null;
         }
         CheatSheetElement result = new CheatSheetElement(nameString);
         if (initializeCheatSheet(result, element))
             return result; // ie.- initialization was successful

         return null;
     }

     /**
      * Create and answer a new CheatSheetCollectionElement, configured as a
      * child of <code>parent</code>
      *
      * @return org.eclipse.ui.internal.model.CheatSheetCollectionElement
      * @param parent org.eclipse.ui.internal.model.CheatSheetCollectionElement
      * @param childName java.lang.String
      */
     protected CheatSheetCollectionElement createCollectionElement(CheatSheetCollectionElement parent, String pluginId, String id, String label) {
         CheatSheetCollectionElement newElement = new CheatSheetCollectionElement(pluginId, id, label, parent);

         parent.add(newElement);
         return newElement;
     }

     /**
      * Creates empty element collection. Overrider to fill
      * initial elements, if needed.
      */
     protected AdaptableList createEmptyCheatSheetCollection() {
         return new CheatSheetCollectionElement(null, "root", "root", null); //$NON-NLS-1$//$NON-NLS-2$
 }

     /**
      * Stores a category element for deferred addition.
      */
     private void deferCategory(IConfigurationElement config) {
         // Create category.
 Category category = null;
         try {
             category = new Category(config);
         } catch (CoreException e) {
             CheatSheetPlugin.getPlugin().getLog().log(e.getStatus());
             return;
         }

         // Defer for later processing.
 if (deferCategories == null)
             deferCategories = new ArrayList (20);
         deferCategories.add(category);
     }

     /**
      * Stores a cheatsheet element for deferred addition.
      */
     private void deferCheatSheet(CheatSheetElement element) {
         if (deferCheatSheets == null)
             deferCheatSheets = new ArrayList (50);
         deferCheatSheets.add(element);
     }

     /**
      * Returns the first cheatsheet
      * with a given id.
      */
     public CheatSheetElement findCheatSheet(String id) {
         Object [] cheatsheetsList = getCheatSheets().getChildren();
         for (int nX = 0; nX < cheatsheetsList.length; nX++) {
             CheatSheetCollectionElement collection = (CheatSheetCollectionElement) cheatsheetsList[nX];
             CheatSheetElement element = collection.findCheatSheet(id, true);
             if (element != null)
                 return element;
         }
         return null;
     }
     
     /**
      * Returns the first task editor
      * with a given id.
      */
     public TaskEditorNode findTaskEditor(String id) {
         if (cheatsheets == null) {
             readCheatSheets(); // Ensure that the registry has been read
 }
         return (TaskEditorNode)taskEditors.get(id);
     }
     
     /**
      * Returns the first task explorer
      * with a given id.
      */
     public TaskExplorerNode findTaskExplorer(String id) {
         if (cheatsheets == null) {
             readCheatSheets(); // Ensure that the registry has been read
 }
         return (TaskExplorerNode)taskExplorers.get(id);
     }
     
     /**
      * Get the list of explorer ids
      * @return an iterator for the explorer ids
      */
     public String [] getExplorerIds() {
         if (cheatsheets == null) {
             readCheatSheets(); // Ensure that the registry has been read
 }
         Set keys = taskExplorers.keySet();
         return (String []) keys.toArray(new String [keys.size()]);
     }

     /**
      * Finishes the addition of categories. The categories are sorted and
      * added in a root to depth traversal.
      */
     private void finishCategories() {
         // If no categories just return.
 if (deferCategories == null)
             return;

         // Sort categories by flattened name.
 CategoryNode[] flatArray = new CategoryNode[deferCategories.size()];
         for (int i = 0; i < deferCategories.size(); i++) {
             flatArray[i] = new CategoryNode((Category) deferCategories.get(i));
         }
         Sorter sorter = new Sorter() {
             private Collator collator = Collator.getInstance();

             public boolean compare(Object o1, Object o2) {
                 String s1 = ((CategoryNode) o1).getPath();
                 String s2 = ((CategoryNode) o2).getPath();
                 return collator.compare(s2, s1) > 0;
             }
         };
         Object [] sortedCategories = sorter.sort(flatArray);

         // Add each category.
 for (int nX = 0; nX < sortedCategories.length; nX++) {
             Category cat = ((CategoryNode) sortedCategories[nX]).getCategory();
             finishCategory(cat);
         }

         // Cleanup.
 deferCategories = null;
     }

     /**
      * Save new category definition.
      */
     private void finishCategory(Category category) {
         CheatSheetCollectionElement currentResult = (CheatSheetCollectionElement) cheatsheets;

         String [] categoryPath = category.getParentPath();
         CheatSheetCollectionElement parent = currentResult; // ie.- root

         // Traverse down into parent category.
 if (categoryPath != null) {
             for (int i = 0; i < categoryPath.length; i++) {
                 CheatSheetCollectionElement tempElement = getChildWithID(parent, categoryPath[i]);
                 if (tempElement == null) {
                     // The parent category is invalid. By returning here the
 // category will be dropped and any cheatsheet within the category
 // will be added to the "Other" category.
 return;
                 }
                 parent = tempElement;
             }
         }

         // If another category already exists with the same id ignore this one.
 Object test = getChildWithID(parent, category.getId());
         if (test != null)
             return;

         if (parent != null)
             createCollectionElement(parent, category.getPluginId(), category.getId(), category.getLabel());
     }

     /**
      * Insert the passed cheatsheet element into the cheatsheet collection appropriately
      * based upon its defining extension's CATEGORY tag value
      *
      * @param element CheatSheetElement
      * @param extension
      * @param currentResult CheatSheetCollectionElement
      */
     private void finishCheatSheet(CheatSheetElement element, IConfigurationElement config, AdaptableList result) {
         CheatSheetCollectionElement currentResult = (CheatSheetCollectionElement) result;
         StringTokenizer familyTokenizer = new StringTokenizer (getCategoryStringFor(config), CATEGORY_SEPARATOR);

         // use the period-separated sections of the current CheatSheet's category
 // to traverse through the NamedSolution "tree" that was previously created
 CheatSheetCollectionElement currentCollectionElement = currentResult; // ie.- root
 boolean moveToOther = false;

         while (familyTokenizer.hasMoreElements()) {
             CheatSheetCollectionElement tempCollectionElement = getChildWithID(currentCollectionElement, familyTokenizer.nextToken());

             if (tempCollectionElement == null) { // can't find the path; bump it to uncategorized
 moveToOther = true;
                 break;
             }
             currentCollectionElement = tempCollectionElement;
         }

         if (moveToOther)
             moveElementToUncategorizedCategory(currentResult, element);
         else
             currentCollectionElement.add(element);
     }

     /**
      * Finishes the addition of cheatsheets. The cheatsheets are processed and categorized.
      */
     private void finishCheatSheets() {
         if (deferCheatSheets != null) {
             Iterator iter = deferCheatSheets.iterator();
             while (iter.hasNext()) {
                 CheatSheetElement cheatsheet = (CheatSheetElement) iter.next();
                 IConfigurationElement config = cheatsheet.getConfigurationElement();
                 finishCheatSheet(cheatsheet, config, cheatsheets);
             }
             deferCheatSheets = null;
         }
     }

     /**
      * Return the appropriate category (tree location) for this CheatSheet.
      * If a category is not specified then return a default one.
      */
     protected String getCategoryStringFor(IConfigurationElement config) {
         String result = config.getAttribute(ATT_CATEGORY);
         if (result == null)
             result = UNCATEGORIZED_CHEATSHEET_CATEGORY;

         return result;
     }

     /**
      * Returns a list of cheatsheets, project and not.
      *
      * The return value for this method is cached since computing its value
      * requires non-trivial work.
      */
     public AdaptableList getCheatSheets() {
         if (cheatsheets == null)
             readCheatSheets();
         return cheatsheets;
     }

     /**
      * Go through the children of the passed parent and answer the child
      * with the passed name. If no such child is found then return null.
      *
      * @return org.eclipse.ui.internal.model.CheatSheetCollectionElement
      * @param parent org.eclipse.ui.internal.model.CheatSheetCollectionElement
      * @param childName java.lang.String
      */
     protected CheatSheetCollectionElement getChildWithID(CheatSheetCollectionElement parent, String id) {
         Object [] children = parent.getChildren();
         for (int i = 0; i < children.length; ++i) {
             CheatSheetCollectionElement currentChild = (CheatSheetCollectionElement) children[i];
             if (currentChild.getId().equals(id))
                 return currentChild;
         }
         return null;
     }

     /**
      * Initialize the passed element's properties based on the contents of
      * the passed registry. Answer a boolean indicating whether the element
      * was able to be adequately initialized.
      *
      * @return boolean
      * @param element CheatSheetElement
      * @param extension Extension
      */
     protected boolean initializeCheatSheet(CheatSheetElement element, IConfigurationElement config) {
         element.setID(config.getAttribute(ATT_ID));
         element.setDescription(getDescription(config));
         element.setConfigurationElement(config);
         element.setRegistered(true);

         String contentFile = config.getAttribute(ATT_CONTENTFILE);
         if (contentFile != null) {
             element.setContentFile(contentFile);
         }

         // ensure that a contentfile was specified
 if (element.getConfigurationElement() == null || element.getContentFile() == null) {
             logMissingAttribute(config, ATT_CONTENTFILE);
             return false;
         }

         String listenerClass = config.getAttribute(ATT_LISTENERCLASS);
         if (listenerClass != null) {
             element.setListenerClass(listenerClass);
         }
         String composite = config.getAttribute(ATT_COMPOSITE);
         if (composite != null) {
             element.setComposite(composite.equalsIgnoreCase(trueString));
         }
         return true;
     }

     /**
      * Moves given element to "Other" category, previously creating one if missing.
      */
     protected void moveElementToUncategorizedCategory(CheatSheetCollectionElement root, CheatSheetElement element) {
         CheatSheetCollectionElement otherCategory = getChildWithID(root, UNCATEGORIZED_CHEATSHEET_CATEGORY);

         if (otherCategory == null)
             otherCategory = createCollectionElement(root, null, UNCATEGORIZED_CHEATSHEET_CATEGORY, UNCATEGORIZED_CHEATSHEET_CATEGORY_LABEL);

         otherCategory.add(element);
     }

     /**
      * Removes the empty categories from a cheatsheet collection.
      */
     private void pruneEmptyCategories(CheatSheetCollectionElement parent) {
         Object [] children = parent.getChildren();
         for (int nX = 0; nX < children.length; nX++) {
             CheatSheetCollectionElement child = (CheatSheetCollectionElement) children[nX];
             pruneEmptyCategories(child);
         }
     }

     /**
      * Reads the cheatsheets in a registry.
      * <p>
      * This implementation uses a defering strategy. All of the elements
      * (categories, cheatsheets) are read. The categories are created as the read occurs.
      * The cheatsheets are just stored for later addition after the read completes.
      * This ensures that cheatsheet categorization is performed after all categories
      * have been read.
      * </p>
      */
     protected void readCheatSheets() {
         IExtensionRegistry xregistry = Platform.getExtensionRegistry();

         if (cheatsheets == null) {
             cheatsheets = createEmptyCheatSheetCollection();
             readRegistry(xregistry, ICheatSheetResource.CHEAT_SHEET_PLUGIN_ID, CHEAT_SHEET_CONTENT);
         }

         finishCategories();
         finishCheatSheets();

         if (cheatsheets != null) {
             CheatSheetCollectionElement parent = (CheatSheetCollectionElement) cheatsheets;
             pruneEmptyCategories(parent);
         }
     }

     public ArrayList readItemExtensions() {
         if (cheatsheetItemExtensions == null) {
             cheatsheetItemExtensions = new ArrayList ();

             IExtensionRegistry xregistry = Platform.getExtensionRegistry();
             //Now read the cheat sheet extensions.
 readRegistry(xregistry, ICheatSheetResource.CHEAT_SHEET_PLUGIN_ID, csItemExtension);
         }

         return cheatsheetItemExtensions;
     }

     private void createItemExtensionElement(IConfigurationElement element) {
         String className = element.getAttribute(ATT_CLASS);
         String itemAttribute = element.getAttribute(ATT_ITEM_ATTRIBUTE);

         // ensure that a class was specified
 if (className == null) {
             logMissingAttribute(element, ATT_CLASS);
             return;
         }
         // ensure that a itemAttribute was specified
 if (itemAttribute == null) {
             logMissingAttribute(element, ATT_ITEM_ATTRIBUTE);
             return;
         }

         CheatSheetItemExtensionElement itemExtensionElement = new CheatSheetItemExtensionElement();
         itemExtensionElement.setClassName(className);
         itemExtensionElement.setItemAttribute(itemAttribute);
         itemExtensionElement.setConfigurationElement(element);

         cheatsheetItemExtensions.add(itemExtensionElement);
     }
     
     /*
      * Get a required attribute. Log an error if it has no value.
      */
     private String getAndCheckAttribute(IConfigurationElement element, String name) {
         String result = element.getAttribute(name);
         if (result == null) {
             logMissingAttribute(element, name);
         }
         return result;
     }
     
     private void createTaskExplorerElement(IConfigurationElement element) {
         String icon = element.getAttribute(ATT_ICON);
         String className = getAndCheckAttribute(element, ATT_CLASS);
         String name = getAndCheckAttribute(element, ATT_NAME);
         String id = getAndCheckAttribute(element, ATT_ID);
         String pluginId = element.getContributor().getName();
         if (id != null && className != null && name != null ) {
             TaskExplorerNode node = new TaskExplorerNode();
             node.setId(id);
             node.setIconPath(icon);
             node.setClassName(className);
             node.setName(name);
             node.setPluginId(pluginId);
             taskExplorers.put(id, node);
         }
     }

     private void createTaskEditorElement(IConfigurationElement element) {
         String icon = getAndCheckAttribute(element, ATT_ICON);
         String className = getAndCheckAttribute(element, ATT_CLASS);
         String id = getAndCheckAttribute(element, ATT_ID);
         String pluginId = element.getContributor().getName();
         if (id != null && className != null && icon != null ) {
             TaskEditorNode node = new TaskEditorNode();
             node.setId(id);
             node.setIconPath(icon);
             node.setClassName(className);
             node.setPluginId(pluginId);
             taskEditors.put(id, node);
         }
     }

     /**
      * Implement this method to read element attributes.
      */
     protected boolean readElement(IConfigurationElement element) {
         if (element.getName().equals(TAG_CATEGORY)) {
             deferCategory(element);
             return true;
         } else if (element.getName().equals(TAG_ITEM_EXTENSION)) {
             createItemExtensionElement(element);
             return true;
         } else if (element.getName().equals(TAG_TASK_EDITOR)) {
             createTaskEditorElement(element);
             return true;
         } else if (element.getName().equals(TAG_TASK_EXPLORER)) {
             createTaskExplorerElement(element);
             return true;
         } else {
             if (!element.getName().equals(TAG_CHEATSHEET))
                 return false;

             CheatSheetElement cheatsheet = createCheatSheetElement(element);
             if (cheatsheet != null)
                 addNewElementToResult(cheatsheet, element, cheatsheets);
             return true;
         }
     }

     /* (non-Javadoc)
      * @see org.eclipse.core.runtime.IRegistryChangeListener#registryChanged(org.eclipse.core.runtime.IRegistryChangeEvent)
      */
     public void registryChanged(IRegistryChangeEvent event) {
         IExtensionDelta[] cheatSheetDeltas = event.getExtensionDeltas(ICheatSheetResource.CHEAT_SHEET_PLUGIN_ID, CHEAT_SHEET_CONTENT);
         if (cheatSheetDeltas.length > 0) {
             // reset the list of cheat sheets, it will be build on demand
 cheatsheets = null;
         }

         IExtensionDelta[] itemExtensionDeltas = event.getExtensionDeltas(ICheatSheetResource.CHEAT_SHEET_PLUGIN_ID, csItemExtension);
         if (itemExtensionDeltas.length > 0) {
             // reset the list of cheat sheets item extensions, it will be build on demand
 cheatsheetItemExtensions = null;
         }
     }
     
     public void stop() {
         IExtensionRegistry xregistry = Platform.getExtensionRegistry();
         xregistry.removeRegistryChangeListener(instance);

         instance = null;
     }
 }

